/**
 * @file main.cpp
 * @author 周晨阳 (2922462750@qq.com)
 * @brief
 * @version 0.1
 * @date 2024-06-11
 *
 * @copyright Copyright (c) 2024
 *
 */
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include <WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <AHT10.h>
#include <ADXL345_WE.h>
#include "EEPROM.h"
#define BLINKER_WIFI
#define BLINKER_MIOT_SENSOR
#include <Blinker.h>
#include <Preferences.h>

#define LENGTH(x) (strlen(x) + 1) // length of char string
#define WiFi_rst 0                // WiFi credential reset pin (Boot button on ESP32)
// Define the number of devices we have in the chain and the hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 4
#define CLK_PIN 2
#define DATA_PIN 3
#define CS_PIN 4
#define ADXL345_I2CADDR 0x53 // 0x1D if SDO = HIGH
// 温湿度显示时长
#define DURATION 3000

String ssid; // string variable to store ssid
String pss;  // string variable to store password
const int int1Pin = 2;
volatile bool tap = false;
//网络状态
bool netState = false;
// 配置NTP客户端
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "ntp.aliyun.com", 28800, 60000);
// HARDWARE SPI
MD_Parola P = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
// SOFTWARE SPI
// MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);
MD_MAX72XX *pM;
AHT10 myAHT20(AHT10_ADDRESS_0X38, AHT20_SENSOR);
ADXL345_WE myAcc = ADXL345_WE(ADXL345_I2CADDR);

// Display parameters
const uint8_t SCROLL_SPEED = 50; // text speed
const textEffect_t SCROLL_EFFECT = PA_NO_EFFECT;
const textPosition_t SCROLL_ALIGN = PA_CENTER;
const uint16_t SCROLL_PAUSE = 0; // in milliseconds

const uint8_t TEXT_ZONE = 0; // just change this to adjust display
const uint8_t TXT_LOWER = 0;
const uint8_t TXT_UPPER = 3;

char message[] = {"08:00:00:00:00:00"};

BlinkerNumber HUMI("humi");
BlinkerNumber TEMP("temp");
BlinkerSlider minBri("minBri");
BlinkerSlider maxBri("maxBri");
BlinkerSlider breathe("breathe");
BlinkerSlider tempDisTime("tempDisTime");
BlinkerButton mySwitch("mySwitch");

float temp, humidity;
// 定义呼吸灯周期和亮暗程度
int breathePeriod = 2000; // 2 秒钟一个周期
int minBrightness = 0;
int maxBrightness = 6;
// 温湿度显示时长
int TempDisTime = DURATION;
// 屏幕开关状态
bool displayState = true;
int nightStart = 23;
int nightEnd = 7;
bool nightModeOn = true;
// 为1时显示温湿度，为0显示时间
int displayMode = 0;
String formattedTime;
// 参数持久化
Preferences preferences;
int limit(int value, int minValue, int maxValue)
{
  if (value < minValue)
  {
    return minValue; // 返回下限
  }
  else if (value > maxValue)
  {
    return maxValue; // 返回上限
  }
  else
  {
    return value; // 返回原值
  }
}
void tempQuery(uint8_t value)
{
  BlinkerMIOT.temp(temp);
  BlinkerMIOT.print();
}
void humiQuery(uint8_t value)
{
  BlinkerMIOT.humi(humidity);
  BlinkerMIOT.print();
}
void miotQuery(int32_t queryCode)
{
  BLINKER_LOG("MIOT Query codes: ", queryCode);

  switch (queryCode)
  {
  case BLINKER_CMD_QUERY_HUMI_NUMBER:
    BLINKER_LOG("MIOT Query HUMI");
    BlinkerMIOT.humi(humidity);
    BlinkerMIOT.print();
    break;
  case BLINKER_CMD_QUERY_TEMP_NUMBER:
    BLINKER_LOG("MIOT Query TEMP");
    BlinkerMIOT.temp(temp);
    BlinkerMIOT.print();
    break;
  default:
    BlinkerMIOT.temp(temp);
    BlinkerMIOT.humi(humidity);
    BlinkerMIOT.print();
    break;
  }
}
void trigDisplay()
{
  displayState = !displayState;
}
void heartbeat()
{
  HUMI.print(humidity);
  TEMP.print(temp);
  minBri.print(minBrightness);
  maxBri.print(maxBrightness);
  breathe.print(breathePeriod);
  tempDisTime.print(TempDisTime);
  mySwitch.print(displayState ? "on" : "off");
}

void tapISR()
{
  tap = true;
}
void saveDisSetting()
{
  preferences.begin("DisSetting", false);
  preferences.putInt("minBri", minBrightness);
  preferences.putInt("maxBri", maxBrightness);
  preferences.putInt("breathe", breathePeriod);
  preferences.putInt("tempDisTime", TempDisTime);
  preferences.end();
}
void loadDisSetting()
{
  preferences.begin("DisSetting", true);
  minBrightness = preferences.getInt("minBri", 0);
  maxBrightness = preferences.getInt("maxBri", 6);
  breathePeriod = preferences.getInt("breathe", 2000);
  TempDisTime = preferences.getInt("tempDisTime", 3000);
  preferences.end();
}
void dataRead(const String &data)
{
  BLINKER_LOG("Blinker readString: ", data);

  Blinker.vibrate();

  uint32_t BlinkerTime = millis();

  Blinker.print("millis", BlinkerTime);
}

void minBriCallback(int32_t value)
{
  minBrightness = value;
  saveDisSetting();
}
void maxBriCallback(int32_t value)
{
  maxBrightness = value;
  saveDisSetting();
}
void breatheCallback(int32_t value)
{

  breathePeriod = limit(value, 100, 5000);
  saveDisSetting();
}
void tempDisTimeCallback(int32_t value)
{
  TempDisTime = limit(value, 1000, 10000);
  saveDisSetting();
}
void powerSwitch(const String &data)
{
  displayState = !displayState;
  mySwitch.print(displayState ? "on" : "off");
}
/**
 * @brief 夜间模式
 * @bug 和单击功能逻辑冲突
 */
void nightMode()
{
  if (!nightModeOn)
  {
    return;
  }

  if (timeClient.getHours() > nightStart || timeClient.getHours() < nightEnd)
  {
    displayState = 0;
    return;
  }
  else
  {
    displayState = 1;
  }
}
/**
 * @brief 初始化函数
 *
 */
void setup(void)
{
  Serial.begin(115200);
  delay(3500);

  Serial.println("matrixClock V1.0");
  preferences.begin("Settings", false);
  P.begin();
  P.setZone(TEXT_ZONE, TXT_LOWER, TXT_UPPER);
  P.setIntensity(15);
  pM = P.getGraphicObject();
  strcpy(message, "Wifi...");

  P.displayZoneText(TEXT_ZONE, message, SCROLL_ALIGN, SCROLL_SPEED, SCROLL_PAUSE, SCROLL_EFFECT, SCROLL_EFFECT);
  P.displayAnimate();

  pinMode(WiFi_rst, INPUT_PULLUP);

  Serial.println("reading wifi info ");
  ssid = preferences.getString("SSID");
  Serial.print("SSID = ");
  Serial.println(ssid);
  pss = preferences.getString("PASS"); // Read Password
  Serial.print("psss = ");
  Serial.println(pss);

  unsigned char retry = 5;
  while (retry > 0 && WiFi.status() != WL_CONNECTED)
  {
    retry--;
    WiFi.begin(ssid.c_str(), pss.c_str());
    delay(1000);
    Blinker.begin("4ab9e7f09a70", ssid.c_str(), pss.c_str());
    delay(1000);
    Serial.println("retry");
  }

  Blinker.attachData(dataRead);
  Blinker.attachHeartbeat(heartbeat);
  minBri.attach(minBriCallback);
  maxBri.attach(maxBriCallback);
  breathe.attach(breatheCallback);
  tempDisTime.attach(tempDisTimeCallback);
  minBri.print(minBrightness);
  maxBri.print(maxBrightness);
  breathe.print(breathePeriod);
  tempDisTime.print(TempDisTime);
  mySwitch.attach(powerSwitch);
  BlinkerMIOT.attachQuery(miotQuery);
  BlinkerMIOT.attachTemp(tempQuery);
  BlinkerMIOT.attachHumi(humiQuery);
  mySwitch.print(displayState ? "on" : "off");

  if (WiFi.status() != WL_CONNECTED) // if WiFi is not connected
  {
    Serial.println("wifi init");

    // Init WiFi as Station, start SmartConfig
    WiFi.mode(WIFI_AP_STA);
    WiFi.beginSmartConfig();

    // Wait for SmartConfig packet from mobile
    Serial.println("Waiting for SmartConfig.");
    retry = 10;
    while (retry > 0 && !WiFi.smartConfigDone())
    {
      strcpy(message, "Wifi.");
      P.displayZoneText(TEXT_ZONE, message, SCROLL_ALIGN, SCROLL_SPEED, SCROLL_PAUSE, SCROLL_EFFECT, SCROLL_EFFECT);
      P.displayAnimate();
      delay(500);
      strcpy(message, "Wifi..");
      P.displayZoneText(TEXT_ZONE, message, SCROLL_ALIGN, SCROLL_SPEED, SCROLL_PAUSE, SCROLL_EFFECT, SCROLL_EFFECT);
      P.displayAnimate();
      delay(500);
      strcpy(message, "Wifi...");
      P.displayZoneText(TEXT_ZONE, message, SCROLL_ALIGN, SCROLL_SPEED, SCROLL_PAUSE, SCROLL_EFFECT, SCROLL_EFFECT);
      P.displayAnimate();
      delay(500);
      Serial.print(".");
      retry--;
    }

    if (!WiFi.smartConfigDone())
    {
      Serial.println("SmartConfig failed.");
      strcpy(message, "ERR 1");
      P.displayZoneText(TEXT_ZONE, message, SCROLL_ALIGN, SCROLL_SPEED, SCROLL_PAUSE, SCROLL_EFFECT, SCROLL_EFFECT);
      P.displayAnimate();
      delay(1000);
      ESP.restart(); // 重启ESP32
    }
    else
    {
      Serial.println("SmartConfig Success.");
      Serial.println("");
      Serial.println("SmartConfig received.");

      strcpy(message, "Wifi OK");
      P.displayZoneText(TEXT_ZONE, message, SCROLL_ALIGN, SCROLL_SPEED, SCROLL_PAUSE, SCROLL_EFFECT, SCROLL_EFFECT);
      P.displayAnimate();
      delay(500);
    }

    // Wait for WiFi to connect to AP
    Serial.println("Waiting for WiFi");
    while (WiFi.status() != WL_CONNECTED)
    {
      delay(500);
      Serial.print(".");
    }
    netState = true;
    Serial.println("WiFi Connected.");

    Serial.print("IP Address: ");
    Serial.println(WiFi.localIP());

    // read the connected WiFi SSID and password
    ssid = WiFi.SSID();
    pss = WiFi.psk();
    Serial.print("SSID:");
    Serial.println(ssid);
    Serial.print("PSS:");
    Serial.println(pss);
    Serial.println("Store SSID & PSS in Flash");
    preferences.putString("SSID", ssid);
    preferences.putString("PASS", pss);
  }
  else
  {
    Serial.println("WiFi Connected");
    netState = true;
  }
  preferences.end();

  pinMode(int1Pin, INPUT);

  delay(100);

  // 初始化NTP客户端
  timeClient.begin();
  // 更新时间
  timeClient.update();

  // 获取当前时间
  formattedTime = timeClient.getFormattedTime();
  strcpy(message, formattedTime.substring(0, 5).c_str());
  P.displayZoneText(TEXT_ZONE, message, SCROLL_ALIGN, SCROLL_SPEED, SCROLL_PAUSE, SCROLL_EFFECT, SCROLL_EFFECT);

  // 输出当前时间
  Serial.println(formattedTime);
  P.displayAnimate();

  while (myAHT20.begin() != true)
  {
    Serial.println(F("AHT20 not connected or fail to load calibration coefficient")); //(F()) save string to flash & keeps dynamic memory free
    strcpy(message, "ERR 2");

    P.displayZoneText(TEXT_ZONE, message, SCROLL_ALIGN, SCROLL_SPEED, SCROLL_PAUSE, SCROLL_EFFECT, SCROLL_EFFECT);
    P.displayAnimate();

    delay(1000);
  }
  Serial.println(F("AHT20 OK"));

  while (!myAcc.init())
  {
    Serial.println("ADXL345 not connected!");
    strcpy(message, "ERR 3");

    P.displayZoneText(TEXT_ZONE, message, SCROLL_ALIGN, SCROLL_SPEED, SCROLL_PAUSE, SCROLL_EFFECT, SCROLL_EFFECT);
    P.displayAnimate();
    delay(1000);
  }
  myAcc.setDataRate(ADXL345_DATA_RATE_200);
  Serial.print("Data rate: ");
  Serial.print(myAcc.getDataRateAsString());

  myAcc.setRange(ADXL345_RANGE_2G);
  Serial.print("  /  g-Range: ");
  Serial.println(myAcc.getRangeAsString());
  Serial.println();

  attachInterrupt(digitalPinToInterrupt(int1Pin), tapISR, RISING);
  myAcc.setGeneralTapParameters(ADXL345_XYZ, 3.0, 30, 100.0);
  myAcc.setAdditionalDoubleTapParameters(false, 250);
  myAcc.setInterrupt(ADXL345_DOUBLE_TAP, INT_PIN_2);
  myAcc.setInterrupt(ADXL345_SINGLE_TAP, INT_PIN_2);

  myAcc.readAndClearInterrupts();

  loadDisSetting();
  formattedTime = timeClient.getFormattedTime();
  strcpy(message, formattedTime.substring(0, 5).c_str());
  P.displayReset();
}

/**
 * @brief 更新温湿度
 *
 */
void readTemp()
{
  static unsigned long previousMillis = 0; // 上次执行时间
  const long interval = 5000;              // 5000毫秒间隔

  // 检查是否到达间隔时间
  if (millis() - previousMillis >= interval)
  {
    previousMillis = millis(); // 更新上次执行时间

    while (myAHT20.readTemperature(AHT10_FORCE_READ_DATA) == 0xff)
    {
      Serial.println(F("AHT20 not connected ")); //(F()) save string to flash & keeps dynamic memory free
      strcpy(message, "ERR 2");

      P.displayZoneText(TEXT_ZONE, message, SCROLL_ALIGN, SCROLL_SPEED, SCROLL_PAUSE, SCROLL_EFFECT, SCROLL_EFFECT);
      P.displayAnimate();

      delay(1000);
    }

    temp = myAHT20.readTemperature(AHT10_FORCE_READ_DATA);
    humidity = myAHT20.readHumidity(AHT10_USE_READ_DATA);

    TEMP.print(temp);
    HUMI.print(humidity);
  }
}
/**
 * @brief 时钟当前方向检测
 *
 */
void positionDetect()
{
  static unsigned long previousMillis = 0; // 获取当前时间
  static int lastPosition = myAcc.getOrientation();
  // 检查是否到达间隔时间
  if (millis() - previousMillis >= 500)
  {
    previousMillis = millis(); // 更新上次执行时间
    // Serial.println(myAcc.getOrientationAsString());
    // 检测到平放，关闭显示
    if (myAcc.getOrientation() == FLAT && lastPosition != FLAT)
    {
      displayState = 0;
      lastPosition = FLAT;
    }
    else if (lastPosition == FLAT && myAcc.getOrientation() != FLAT)
    {
      trigDisplay();
      lastPosition = myAcc.getOrientation();
    }
  }
}
/**
 * @brief 拍击检测
 *
 */
void tapDetect()
{
  if (tap == true)
  {
    // byte actTapSource = myAcc.getActTapStatus();
    // Serial.println(actTapSource, BIN);
    String axes = myAcc.getActTapStatusAsString();
    byte intSource = myAcc.readAndClearInterrupts();
    // 错误检测
    while (intSource == 0)
    {
      Serial.println("ADXL345 not connected!");
      strcpy(message, "ERR 3");
      P.displayZoneText(TEXT_ZONE, message, SCROLL_ALIGN, SCROLL_SPEED, SCROLL_PAUSE, SCROLL_EFFECT, SCROLL_EFFECT);
      P.displayAnimate();
      delay(1000);
      intSource = myAcc.readAndClearInterrupts();
    }
    // 双击检测
    if (myAcc.checkInterrupt(intSource, ADXL345_DOUBLE_TAP))
    {
      Serial.print("DOUBLE TAP at: ");
      Serial.println(axes);
      displayMode = 1;
    }
    // 单击检测
    else if (myAcc.checkInterrupt(intSource, ADXL345_SINGLE_TAP))
    {
      Serial.print("SINGLE TAP at: ");
      Serial.println(axes);
      // displayState = !displayState;
    }

    myAcc.readAndClearInterrupts();
    tap = false;
  }
}
/**
 * @brief 实时时间更新
 *
 */
void timeUpdate() {
  static unsigned long timeLastUpdateTime = 0;
  static int lastHour = 0, lastMinute = 0, lastSecond = 0;  // 最后成功获取的时分秒
  static unsigned long lastSuccessMillis = 0;              // 最后成功获取时间的毫秒戳

  unsigned long nowMillis = millis();

  // 每隔50秒尝试更新网络时间
  if (nowMillis - timeLastUpdateTime >= 50000) {
    if (netState) {  // 网络有效
      // 获取并保存当前网络时间
      lastHour = timeClient.getHours();
      lastMinute = timeClient.getMinutes();
      lastSecond = timeClient.getSeconds();
      lastSuccessMillis = nowMillis;  // 记录成功时刻的毫秒数
      timeLastUpdateTime = nowMillis;
    }
    // 网络更新失败时，不修改lastSuccessMillis，继续使用本地计时
  }

  // 计算从最后成功时间到现在的流逝时间（毫秒转秒）
  if (lastSuccessMillis != 0) {  // 确保有初始时间
    //如果网络正常，则nowMillis与lastSuccessMillis相等
    unsigned long elapsedMillis = nowMillis - lastSuccessMillis;
    unsigned long elapsedSeconds = elapsedMillis / 1000;
    
    // 计算累计总秒数并处理进位
    unsigned long totalSeconds = lastSecond + elapsedSeconds;
    int seconds = totalSeconds % 60;
    unsigned long totalMinutes = totalSeconds / 60;
    int minutes = (lastMinute + totalMinutes) % 60;
    int hours = (lastHour + (lastMinute + totalMinutes) / 60) % 24;  // 处理小时进位（24小时制）

    // 格式化为HH:MM字符串
    String formatted = String(hours < 10 ? "0" : "") + String(hours) + ":" +
                      String(minutes < 10 ? "0" : "") + String(minutes);
    formattedTime = formatted;
  } else {
    // 首次运行或未获取过时间（可选处理，根据实际需求）
    formattedTime = "00:00";
  }

  // 原有功能：输出和显示时间
  Serial.println(formattedTime);
  strcpy(message, formattedTime.substring(0, 5).c_str());
  P.displayReset();
}
/**
 * @brief 显示循环
 *
 */
void displayLoop()
{
  static unsigned long tempLastUpdateTime = 0;

  // 计算当前时间在呼吸周期内的百分比
  unsigned long currentMillis = millis();
  breathePeriod = limit(breathePeriod, 100, 10000);
  float percentage = (float)(currentMillis % breathePeriod) / breathePeriod;

  // 根据百分比计算当前亮度
  int brightness;
  if (percentage < 0.5)
  { // 从暗到亮
    brightness = minBrightness + (maxBrightness - minBrightness) * 2 * percentage;
  }
  else
  { // 从亮到暗
    brightness = maxBrightness - (maxBrightness - minBrightness) * 2 * (percentage - 0.5);
  }
  brightness = limit(brightness, 0, 15);
  // 设置 LED 亮度
  P.setIntensity(brightness);

  // 延迟 10 毫秒,让呼吸效果更平滑
  delay(10);
  // 温度显示
  if (displayMode == 1)
  {
    sprintf(message, "%d'%d%%", (int)temp, (int)humidity);
    P.displayReset();
    delay(30);
    // 显示时长超时后
    if (millis() - tempLastUpdateTime >= TempDisTime)
    {
      displayMode = 0;
      strcpy(message, formattedTime.substring(0, 5).c_str());
      P.displayReset();
    }
  }
  else if (displayMode == 2)
  {
    // sprintf(message, "%ld", timeClient.getEpochTime());
    // P.displayReset();
    // delay(30);
    // // 显示时长超时后
    // if (millis() - tempLastUpdateTime >= TempDisTime)
    // {
    //   displayMode = 0;
    //   strcpy(message, formattedTime.substring(0, 5).c_str());
    //   P.displayReset();
    // }
  }

  else
  {
    tempLastUpdateTime = millis();
  }
}
/**
 * @brief 长按重置WIFI检测
 *
 */
void resetDetect()
{
  static unsigned long rst_millis;

  // 长按重置WIFI检测
  rst_millis = millis();
  while (digitalRead(WiFi_rst) == LOW)
  {
    // Wait till boot button is pressed

    // check the button press time if it is greater than 3sec clear wifi cred and restart ESP
    if (millis() - rst_millis >= 5000)
    {
      strcpy(message, "Reset..");
      P.displayReset();
      P.displayAnimate();

      Serial.println("Reseting the WiFi credentials");
      preferences.begin("Settings", false);
      preferences.putString("SSID", "null");
      preferences.putString("PASS", "null");
      preferences.end();
      Serial.println("Wifi credentials erased");
      Serial.println("Restarting the ESP");
      delay(2000);
      ESP.restart(); // Restart ESP
    }
  }
}
/**
 * @brief 检查网络连接状态,并重连
 *
 */
void netloop()
{
  static unsigned long delayMillis = 0;
  if (millis() - delayMillis >= 30000)
  {

    // 检查WiFi连接状态
    if (WiFi.status() == WL_CONNECTED)
    {
      Serial.println("WiFi is connected");
      netState = true;
    }
    else
    {
      Serial.println("WiFi is disconnected, trying to reconnect...");
      netState = false;
      WiFi.reconnect();
    }
    delayMillis = millis(); // 更新上次执行时间
  }
}
void loop(void)
{
  resetDetect();
  readTemp();
  Blinker.run();
  // 时间更新
  timeUpdate();
  positionDetect();
  // 拍击检测
  tapDetect();
  // nightMode();
  if (!displayState)
  {
    P.displayShutdown(true);
    return;
  }

  P.displayShutdown(false);

  P.displayAnimate();

  displayLoop();
  netloop();
}
